//Neon light shader - Ryan Wachowiak, Nathan Popham
#include <stdlib.h>
#include <iostream>
#include <math.h>

#ifdef __APPLE__

#include <GLUT/glut.h>
#else
#include <GL/glew.h>
#include <GL/glut.h>
#include <GL/gl.h>
#endif

#include "ShaderSetup.h"
#include "Point3f.h"

using namespace std;

int	currentX;		// Current location of X
int	currentY;		// Current location of Y
int	startX;			// Starting location of X
int	startY;			// Starting location of Y		

int	windowWidth = 800;		// Current width of window
int	windowHeight = 600;	// Current height of window

float blurSizeX = 1.0/windowWidth;
float blurSizeY = 1.0/windowHeight;

float largeBlurSizeX = 10.0/windowWidth;
float largeBlurSizeY = 10.0/windowHeight;

GLuint frameBuffer = 0;

GLuint blurShader = 0;
GLuint addTexturesShader = 0;

GLfloat backgroundColor[4] = {0, 0, 0, 0};
GLuint neon = 0;
GLuint tube = 0;


bool animate = false;
bool glow = false;

GLfloat WallColors[][3] = {
    { 1.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, 
    { 1.0, 0.0, 0.0 }
};

GLfloat WallVertices[][3] = {
    { -5.0,  5.0,  5.0 }, {  5.0, 5.0,  5.0 }, { 5.0,  -5.0,  5.0 }, 
    { -5.0,  -5.0,  5.0 }
};

GLfloat WallTexCoords[][2] = {
    { 0.0, 1.0 }, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0} 
};

GLfloat FloorColors[][3] = {
    { 1.0, 1.0, 0.5 }, { 1.0, 1.0, 0.5 }, { 1.0, 1.0, 0.5 }, 
    { 1.0, 1.0, 0.5 }
};

GLfloat FloorVertices[][3] = {
    { -5.0, -5.0,  5.0 }, {  5.0, -5.0,  5.0 }, { 5.0,  -5.0,  -5.0 }, 
    { -5.0, -5.0, -5.0 }
};

int degree = 10;//degree of curve
Point3f* bezierCurve = 0;
Point3f* bezierCurve2 = 0;
GLfloat* finalCurve = 0;
int* coef = 0;

int sampleCount = 256;

GLfloat no_mat[]            = { 0.0, 0.0, 0.0, 1.0 };
GLfloat mat_ambient[]       = { 0.7, 0.7, 0.1, 1.0 };
GLfloat mat_ambient_color[] = { 0.5, 0.5, 0.05, 1.0 };
GLfloat mat_diffuse[]       = { 0.5, 0.5, 0.05, 1.0 };
GLfloat mat_specular[]      = { 0.8, 0.8, 0.8, 1.0 };
GLfloat no_shininess[]      = { 0.0 };
GLfloat low_shininess[]     = { 5.0 };
GLfloat mid_shininess[]     = { 50.0 };
GLfloat high_shininess[]    = { 100.0 };
GLfloat mat_emission[]      = { 0.3, 0.2, 0.2, 0.0 };

GLfloat mat_yellow[]        = { 0.5, 0.5, 0.0, 1.0 };
GLfloat mat_red[]           = { 0.5, 0.0, 0.0, 1.0 };
GLfloat mat_green[]         = { 0.0, 0.5, 0.0, 1.0 };
GLfloat mat_blue[]          = { 0.0, 0.0, 0.5, 1.0 };
GLfloat mat_gray[]          = { 0.3, 0.3, 0.3, 1.0 };
GLfloat mat_neon[]          = { 0.5, 0.5, 1.0, 1.0 };

GLint n = 0;
float tubeThickness = .2;
unsigned long time = 0;
GLUquadricObj* cyl = 0;

/*
 * Draws a neon tube, pathed by a random bezier curve. The bezier curve is
 * given dimension by drawing a series of small cylinders along its line.
 */
void drawTube()
{
	glUseProgram(0);
	//this program doesn't exist
	if(tube)
	{
		glUseProgram(tube);
	}
	glMaterialfv( GL_FRONT, GL_AMBIENT, no_mat);
	glMaterialfv( GL_FRONT, GL_DIFFUSE, no_mat);
	glMaterialfv( GL_FRONT, GL_SPECULAR,  mat_specular );
	glMaterialfv( GL_FRONT, GL_SHININESS, high_shininess );
	glMaterialfv( GL_FRONT, GL_EMISSION,  mat_neon );
	float t = 0;
	float tInc = .1 / (sampleCount - 1.0);
	Point3f p;	
	glLineWidth(1.5);
	//sample the bezier curve in a way that keeps points at a uniform density (they aren't spread out at certain points and condenced pointlessly at others)
	for(int i = 0; i < sampleCount; i++)
	{
		bool findingPoint = t <= 1;
		if(t <= 1)
		{
			n = i + 1;
		}
		//increment t by tInc if the generated point is too close to the last one
		while(findingPoint)
		{
			p = Point3f();
			for(int j = 0; j <= degree; j++)
			{
				p += bezierCurve[j] * (coef[j] * pow(t, j) * pow((1 - t), degree - j)) * (1 - (time / 1000.0));
				p += bezierCurve2[j] * (coef[j] * pow(t, j) * pow((1 - t), degree - j)) * (time / 1000.0);
			}
			if(i)
			{
				Point3f last = Point3f(finalCurve[(i - 1) * 3], finalCurve[(i - 1) * 3 + 1], finalCurve[(i - 1) * 3 + 2]);
				float distSqr = p.distanceSqr(last);
				if(distSqr < .0055)
				{
					t += tInc;
				}
				else
				{
					findingPoint = false;
				}
			}
			else
			{
				findingPoint = false;
			}
		}
		//render a cylinder
		if(i)
		{
			//http://lifeofaprogrammergeek.blogspot.com/2008/07/rendering-cylinder-between-two-points.html
			Point3f last = Point3f(finalCurve[(i - 1) * 3], finalCurve[(i - 1) * 3 + 1], finalCurve[(i - 1) * 3 + 2]);
			float h = sqrt(p.distanceSqr(last));
			Point3f vec = p - last;
			cyl = gluNewQuadric();
			gluQuadricDrawStyle(cyl, GLU_FILL);
			glPushMatrix();
			
			if(vec.z == 0)
			{
				vec.z = 0.00001;
			}
			float vLen = sqrt(vec.distanceSqr(Point3f()));
			float az = 180 / 3.14159 * acos(vec.z / vLen);
			if(vec.z < 0)
			{
				az = -az;
			}
			float rx = -vec.y * vec.z;
			float ry = vec.x * vec.z;

			glTranslatef (last.x, last.y, last.z);
			glRotatef(az, rx, ry, 0.0);

			gluCylinder (cyl, tubeThickness, tubeThickness, h, 10, 10);
			glPopMatrix();
		}
		finalCurve[i * 3] = p.x;
		finalCurve[i * 3 + 1] = p.y;
		finalCurve[i * 3 + 2] = p.z;
		t += tInc;
	}
}


//This draws the background floor and applies the neon shader to illuminate it
//when in proximity to the neon tube.
void drawScene()
{
	glUseProgram(0);
	glMaterialfv( GL_FRONT, GL_AMBIENT, mat_gray );
	glMaterialfv( GL_FRONT, GL_DIFFUSE, mat_gray );
	glMaterialfv( GL_FRONT, GL_SPECULAR,  no_mat );
	glMaterialfv( GL_FRONT, GL_SHININESS, no_mat );
	glMaterialfv( GL_FRONT, GL_EMISSION,  no_mat );	
	if(neon)
	{
		glUseProgram(neon);
		GLuint samples = glGetUniformLocation(neon, "samples");
		glUniform3fv(samples, sampleCount, (GLfloat*)finalCurve);
		GLuint neonSh = glGetUniformLocation(neon, "neon");
		glUniform4fv(neonSh, 1, mat_neon);
		GLuint pointCount = glGetUniformLocation(neon, "lastPoint");
		glUniform1iv(pointCount, 1, &n);
	}
	glBegin( GL_POLYGON );

	  glColor3fv(  FloorColors[0]   );
	  glVertex3fv( FloorVertices[0] );

	  glColor3fv(  FloorColors[1]   );
	  glVertex3fv( FloorVertices[1] );

	  glColor3fv(  FloorColors[2]   );
	  glVertex3fv( FloorVertices[2] );

	  glColor3fv(  FloorColors[3]   );
	  glVertex3fv( FloorVertices[3] );

	glEnd( );
}

//Sets up the perspective scene and goes into 
//ModelView mode.
void pointCamera()
{
	 // Camera stuff - matrix initialization
	 glMatrixMode( GL_PROJECTION );
	 glLoadIdentity( );
	 gluPerspective (45, 1.0, 1.0, 100.0);

	 glMatrixMode( GL_MODELVIEW );
	 glLoadIdentity( );
	 gluLookAt (0.0, 15.0, -15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
}

/*
 * Generates a new empty texture and returns its id.
 */
GLuint newTexture()
{
	GLuint textureId = 0;
	glGenTextures(1, &textureId);

	glBindTexture(GL_TEXTURE_2D, textureId);

	glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, windowWidth, windowHeight, 0,GL_RGB, GL_UNSIGNED_BYTE, 0);
 
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

	glBindTexture(GL_TEXTURE_2D, 0);

	return textureId;
}

/*
 * Draws a quad object -- effectively a screen in the scene (rhyme!), then 
 * applies the given texture to it. It also passes along the blur parameters 
 * to the blur shader to blur in one axis direction.
 *
 * blur -- denotes the strength of the blur
 * blurDir -- if 1.0, blur along x-axis. If 0, along y-axis.
 */
void drawTexture(int textureId, float blur, float blurDir)
{	
	glUseProgram(0);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, textureId);
	 
	glUseProgram(blurShader);
	GLuint texloc = glGetUniformLocation(blurShader, "myTexture");
	GLuint blurloc = glGetUniformLocation(blurShader, "blurSize");
	GLuint blurDirLoc = glGetUniformLocation(blurShader, "blurDir");
	glUniform1i(texloc, 0);
	glUniform1f(blurloc, blur);
	glUniform1f(blurDirLoc, blurDir);

	glPushMatrix();
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, windowWidth, 0, windowHeight, 0, 0);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	//Draw a textured quad
	glBegin( GL_QUADS );
		glTexCoord2f(0, 0); 
		glVertex3f(-1, -1, 0);

		glTexCoord2f(0, 1); 
		glVertex3f(-1, 1, 0);

		glTexCoord2f(1, 1); 
		glVertex3f(1, 1, 0);

		glTexCoord2f(1, 0); 
		glVertex3f(1, -1, 0);
	glEnd();

	glUseProgram(0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glPopMatrix();	
}

/**
 * Similar to drawTexture, but adds two textures together and does not blur.
 */
void drawCombinedTextures(GLuint texture1, GLuint texture2)
{
	glPushMatrix();
	glUseProgram(0);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture1);
	
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, texture2);
	 
	glUseProgram(addTexturesShader);
	GLuint tex1loc = glGetUniformLocation(addTexturesShader, "tex1");
	GLuint tex2loc = glGetUniformLocation(addTexturesShader, "tex2");
	glUniform1i(tex1loc, 0);
	glUniform1i(tex2loc, 1);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, windowWidth, 0, windowHeight, 0, 0);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	//Draw a textured quad
	glBegin( GL_QUADS );
		glTexCoord2f(0, 0); 
		glVertex3f(-1, -1, 0);

		glTexCoord2f(0, 1); 
		glVertex3f(-1, 1, 0);

		glTexCoord2f(1, 1); 
		glVertex3f(1, 1, 0);

		glTexCoord2f(1, 0); 
		glVertex3f(1, -1, 0);
	glEnd();

	glUseProgram(0);
	glPopMatrix();	
}

/**
 * This call sets up a framebuffer object with the given textureId and binds it
 * to the screen framebuffer. Everything drawn after this call will be drawn 
 * into the texture, until a matching call to endRenderToTexture().
 */
bool beginRenderToTexture(int textureId)
{
	glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
	glViewport(0,0,windowWidth,windowHeight);

	glBindTexture(GL_TEXTURE_2D, textureId);

	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, textureId, 0);
	

	GLenum DrawBuffers[2] = {GL_COLOR_ATTACHMENT0};
	glDrawBuffers(1, DrawBuffers);

	if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		return false;
	else
		return true;
}

/**
 * Resets the framebuffers to draw to the screen after a call to beginRenderToTexture().
 */
void endRenderToTexture()
{
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glViewport(0, 0, windowWidth, windowHeight);
	glUseProgram(0);
}

/**
 * Generates a new framebuffer.
 */
void initFramebuffer()
{
	//example http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-14-render-to-texture/
	frameBuffer = 0;
	glGenFramebuffers(1, &frameBuffer);

}

/**
 * This function exists due to misguided frustration towards certain animating
 * bug. It remains for the sake of posterity.
 */
void drawShit()
{
	 pointCamera();
	if(glow)
	{
	 GLuint sceneTex = newTexture();
	 glUseProgram(0);
	 beginRenderToTexture(sceneTex);	
	 drawTube();
	 drawScene();
	 drawTube();
	 endRenderToTexture();
	 
	 GLuint neonTex = newTexture();

	 beginRenderToTexture(neonTex);
	 drawTube();
	 endRenderToTexture();


	 GLuint secondTex = newTexture();
	 beginRenderToTexture(secondTex);
	 drawTexture(neonTex, blurSizeX, 1.0);
	 endRenderToTexture();
	 
	 
	 GLuint fullBlurTex = newTexture();
	 beginRenderToTexture(fullBlurTex);
	 drawTexture(secondTex, blurSizeY, 0.0);
	 endRenderToTexture();

	 GLuint mid2Tex = newTexture();
	 beginRenderToTexture(mid2Tex);
	 drawTexture(neonTex, largeBlurSizeX, 1.0);
	 endRenderToTexture();

	 GLuint largeBlurTex = newTexture();
	 beginRenderToTexture(largeBlurTex);
	 drawTexture(mid2Tex, largeBlurSizeY, 0.0);
	 endRenderToTexture();


	 GLuint tempTex = newTexture();
	 beginRenderToTexture(tempTex);
	 drawCombinedTextures(fullBlurTex ,sceneTex);
	 endRenderToTexture();

	 drawCombinedTextures(tempTex, largeBlurTex);

	 glDeleteTextures(1, &sceneTex);
	 glDeleteTextures(1, &neonTex);
	 glDeleteTextures(1, &secondTex);
	 glDeleteTextures(1, &fullBlurTex);
	 glDeleteTextures(1, &mid2Tex);
	 glDeleteTextures(1, &largeBlurTex);
	 glDeleteTextures(1, &tempTex);
	}
	else
	{
		drawTube();
		drawScene();
	}
}

/**
 * The main rendering loop.
 */
void display( void ) 
{
	 glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	 drawShit();
	 
	 glutSwapBuffers();
}


void myReshape( int width, int height ) 
{
	 glViewport( 0, 0, width, height );
	 windowWidth = width;
	 windowHeight = height;
}


//we needed factorial for this
//Vin Diesel once made a go kart that runs off the hopes and dreams of orphans.  It goes fast.
int factorial(int n)
{
	if(n > 1)
	{
		return n * (factorial(n - 1));
	}
	else
	{
		return 1;
	}
}


//we need to make a bezier curve and a 2nd one for the animation
void generateBezier()
{
	for(int i = 0; i <= degree; i++)
	{
		coef[i] = factorial(degree) / (factorial(i) * factorial(degree - i));
		bezierCurve[i].x = rand() * 8.0 / RAND_MAX - 4;
		bezierCurve[i].y = -(rand() * 2.0 / RAND_MAX + 2);
		bezierCurve[i].z = rand() * 8.0 / RAND_MAX - 4;
	}
}
void generateBezier2()
{
	for(int i = 0; i <= degree; i++)
	{
		bezierCurve2[i].x = rand() * 8.0 / RAND_MAX - 4;
		bezierCurve2[i].y = -(rand() * 2.0 / RAND_MAX + 2);
		bezierCurve2[i].z = rand() * 8.0 / RAND_MAX - 4;
	}
}


//allocate memory
void initBezier()
{
	bezierCurve = new Point3f[degree + 1];
	bezierCurve2 = new Point3f[degree + 1];
	finalCurve = new GLfloat[sampleCount * 3];
	coef = new int[degree + 1];
	generateBezier();
	generateBezier2();
}



void init( void )
{
	initBezier();
	initFramebuffer();
	// lighting params
	GLfloat ambient[]        = { 0.2, 0.2, 0.2, 1.0 };
	GLfloat diffuse[]        = { 1.0, 1.0, 1.0, 1.0 };
	GLfloat specular[]       = { 1.0, 1.0, 1.0, 1.0 };
	GLfloat position[]       = { 0.0, 10.0, -8.0, 1.0 };
	GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
	GLfloat local_view[]     = { 0.0 };

   
	glClearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], backgroundColor[3]);

	// enable depth testing
	glEnable( GL_DEPTH_TEST );
	glEnable (GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);	

	// some basic lighting setup
	glLightfv( GL_LIGHT0, GL_AMBIENT, ambient );
	glLightfv( GL_LIGHT0, GL_DIFFUSE, diffuse );
	glLightfv( GL_LIGHT0, GL_POSITION, position );
	glLightModelfv( GL_LIGHT_MODEL_AMBIENT, lmodel_ambient );
	glLightModelfv( GL_LIGHT_MODEL_LOCAL_VIEWER, local_view );
   
	glEnable( GL_LIGHTING );
	glEnable( GL_LIGHT0 );

	// Camera stuff - matrix initialization
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity( );
	gluPerspective (45, 1.0, 1.0, 100.0);

	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity( );
	gluLookAt (0.0, 15.0, -15.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

	// set up your shaders with code like this:
	neon = ShaderSetup( "neon.vert", "neon.frag" );
	if( neon == 0 ) 
	{
		fputs( "Error in shader setup - exiting\n", stderr );
		exit( 1 );
	}

	
	blurShader = ShaderSetup( "blur.vert", "blur.frag" );
	if( blurShader == 0 ) 
	{
		fputs( "Error in shader setup - exiting\n", stderr );
		system("pause");
		exit( 1 );
	}

	addTexturesShader = ShaderSetup( "blur.vert", "addTextures.frag" );
	if( addTexturesShader == 0 ) 
	{
		fputs( "Error in shader setup - exiting\n", stderr );
		system("pause");
		exit( 1 );
	}
}

/**
 * Controls: 
 *  Space: begin or end animation
 *  g: begin or end glow
 *  s: instantly generate a new random bezier curve
 */
void keyboard(unsigned char key, int x, int y)
{
	switch(key)
	{
	case ' ':
		animate = !animate;
		break;
	case 'g':
		glow = !glow;
		break;
	case 's':
		generateBezier();
		break;
	}
	glutPostRedisplay();
}


//animate stuffs
void idle()
{
	if(animate)
	{
		time += 10;
		if(time > 1000)
		{
			time = 0;
			Point3f* temp = bezierCurve;
			bezierCurve = bezierCurve2;
			bezierCurve2 = temp;
			generateBezier2();
		}
		glutPostRedisplay();
	}
}


int main( int argc, char **argv )
{
	// Initialize GLUT
	glutInit( &argc, argv );

	// Enable double buffering and depth buffering
	glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
	glutInitWindowSize( windowWidth, windowHeight );
	glutCreateWindow( argv[0] );

	// Callback functions are specified 
	glutReshapeFunc( myReshape );
	glutDisplayFunc( display );

	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

#ifndef __APPLE__
    glewInit();
#endif

	// Do OpenGL initialization
	init();
	
	// enter your display loop.
	glutMainLoop();
	
	return 0;
}